package bhoost.jsi;

import bhoost.shell.smack_providers.*;

import java.security.AccessController;
import java.security.PrivilegedAction;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Hashtable;

import org.jivesoftware.smack.ConnectionConfiguration;
import org.jivesoftware.smack.PacketListener;
import org.jivesoftware.smack.XMPPConnection;
import org.jivesoftware.smack.XMPPException;
import org.jivesoftware.smack.filter.PacketFilter;
import org.jivesoftware.smack.filter.PacketTypeFilter;
import org.jivesoftware.smack.packet.Message;
import org.jivesoftware.smack.packet.Packet;
import org.jivesoftware.smack.packet.Presence;
import org.jivesoftware.smack.provider.ProviderManager;
import org.jivesoftware.smackx.muc.DiscussionHistory;
import org.jivesoftware.smackx.muc.MultiUserChat;

public class Xmpp implements IXmpp {
  final String resource = "marco-android";
  private String domain = "conference.lablz.com";
  private static XMPPConnection connection;
  private Hashtable<String, MultiUserChat> chats;
  private MultiUserChat currentChat;
  private String username;
  private String password;
  private EventManager eventManager;
  private static boolean send = true;

  private static Xmpp instance;

  private Xmpp() {
    eventManager = EventManager.getInstance();
    chats = new Hashtable<String, MultiUserChat>();
    setSmackProviders();
  }

  public static void doSend(boolean sendMessages) {
    send = sendMessages;
    if (sendMessages && instance == null)
      getInstance();
  }

  public static Xmpp getInstance() {
    if (instance == null)
      instance = new Xmpp();
    return instance;
  }

  public void setHost(String domain) {
    this.domain = domain;
  }

  /**
   * IXmpp method implementation - connect to domain
   */
  public void connect() {
    try {
      if (connection == null)
        configureConnection();
      while (!connection.isConnected()) {
        for (int i = 0; i < 5; i++) {
          logD("not connected");
          try {
            connection.connect();
            logD("connected");
          } catch (XMPPException x) {
            logD("couldn't connect: " + x.getMessage());
          }
          if (connection.isConnected())
            return;
        }
        Thread.sleep(5000);
      }
    } catch (Throwable e) {
      logD("failed in connect(): " + e.getMessage());
    }
  }

  public void disconnect() {
    if (connection == null)
      return;
    connection.disconnect();
    connection = null;
  }

  private void configureConnection() {
    XMPPConnection.DEBUG_ENABLED = false;
    ConnectionConfiguration config = new ConnectionConfiguration(domain, 5222);
    config.setSASLAuthenticationEnabled(false);
    config.setSecurityMode(ConnectionConfiguration.SecurityMode.disabled);
    connection = new XMPPConnection(config);
    logD("configured connection");
  }

  private void setSmackProviders() {
    logD("setting smack providers");
    ProviderManager pm = ProviderManager.getInstance();
    pm.addExtensionProvider("x", "jabber:x:roster",                               new RosterExchangeProvider());
    pm.addExtensionProvider("x", "jabber:x:event",                                new MessageEventProvider());
    pm.addExtensionProvider("active", "http://jabber.org/protocol/chatstates",    new ChatStateExtension.Provider());
    pm.addExtensionProvider("composing", "http://jabber.org/protocol/chatstates", new ChatStateExtension.Provider());
    pm.addExtensionProvider("paused", "http://jabber.org/protocol/chatstates",    new ChatStateExtension.Provider());
    pm.addExtensionProvider("inactive", "http://jabber.org/protocol/chatstates",  new ChatStateExtension.Provider());
    pm.addExtensionProvider("gone", "http://jabber.org/protocol/chatstates",      new ChatStateExtension.Provider());
    pm.addExtensionProvider("html", "http://jabber.org/protocol/xhtml-im",        new XHTMLExtensionProvider());
    pm.addExtensionProvider("x", "jabber:x:conference",                           new GroupChatInvitation.Provider());
    pm.addExtensionProvider("x", "jabber:x:data",                                 new DataFormProvider());
    pm.addExtensionProvider("x", "http://jabber.org/protocol/muc#user",           new MUCUserProvider());
    pm.addExtensionProvider("x", "jabber:x:delay",                                new DelayInformationProvider());
    pm.addExtensionProvider("offline", "http://jabber.org/protocol/offline",      new OfflineMessageInfo.Provider());
    pm.addExtensionProvider("addresses", "http://jabber.org/protocol/address",    new MultipleAddressesProvider());
    pm.addExtensionProvider("data", "http://jabber.org/protocol/ibb",             new IBBProviders.Data());
    pm.addIQProvider("query", "jabber:iq:private",                                new PrivateDataManager.PrivateDataIQProvider());
    pm.addIQProvider("query", "jabber:iq:time",                                   Time.class);
    pm.addIQProvider("query", "http://jabber.org/protocol/disco#items",           new DiscoverItemsProvider());
    pm.addIQProvider("query", "http://jabber.org/protocol/disco#info",            new DiscoverInfoProvider());
    pm.addIQProvider("query", "http://jabber.org/protocol/muc#admin",             new MUCAdminProvider());
    pm.addIQProvider("query", "http://jabber.org/protocol/muc#owner",             new MUCOwnerProvider());
    pm.addIQProvider("query", "jabber:iq:version",                                Version.class);
    pm.addIQProvider("vCard", "vcard-temp",                                       new VCardProvider());
    pm.addIQProvider("offline", "http://jabber.org/protocol/offline",             new OfflineMessageRequest.Provider());
    pm.addIQProvider("query", "jabber:iq:last",                                   new LastActivity.Provider());
    pm.addIQProvider("query", "jabber:iq:search",                                 new UserSearch.Provider());
    pm.addIQProvider("sharedgroup", "http://www.jivesoftware.org/protocol/sharedgroup", new SharedGroupsInfo.Provider());
    pm.addIQProvider("si", "http://jabber.org/protocol/si",                       new StreamInitiationProvider());
    pm.addIQProvider("query", "http://jabber.org/protocol/bytestreams",           new BytestreamsProvider());
    pm.addIQProvider("open", "http://jabber.org/protocol/ibb",                    new IBBProviders.Open());
    pm.addIQProvider("close", "http://jabber.org/protocol/ibb",                   new IBBProviders.Close());
    pm.addIQProvider("query", "jabber:iq:privacy",                                new PrivacyProvider());
  }

  public long getTime(Message msg) {
    logD("trying to get time from message, message null: " + (msg == null));

    if (msg == null)
      return -1;

    Object obj = msg.getProperty("time");
    if (obj == null)
      return -1;

    if (obj instanceof String) {
      String time = (String)obj;
      if (time.trim().length() == 0)
        return -1;

      SimpleDateFormat sdf = new SimpleDateFormat();
      try {
        Date date = sdf.parse(time);
        return date.getTime();
      } catch (ParseException p) {
        return -1;
      }
    }

    if (!(obj instanceof Long))
      return -1;

    Long milliseconds = (Long)obj;
    if (milliseconds == null)
      return -1;

    long millis = milliseconds.longValue();
    return millis;
  }

  public void queueEvent(XmppEvent event) {
    eventManager.queueEvent(event);
  }

  public void logD(String message) {
    System.out.println(message);
  }

  /**
   * IXmpp method implementation - login with name,pwd to conference.lablz.com, and set up listeners on connection
   */
  public void login(String name, String pwd) {
    logD("trying to log in as " + name);

    if (connection != null && connection.isAuthenticated() && !username.equals(name)) {
      connection.disconnect();
      connection = null;
    }
    username = name;
    password = pwd;
    
    if (connection == null || !connection.isConnected())
      connect();

    if (!connection.isAuthenticated()) {
      AccessController.doPrivileged( new PrivilegedAction() {
        public Object run() {
          try {
            connection.login(username, password, resource);
            logD("logged in as " + username);
          } catch (XMPPException x) {
            logD("couldn't connect or login: " + x.getMessage());
          }
          return null; // still need this
        }
      });
    }

    PacketFilter filter = new PacketTypeFilter(Message.class);
    PacketListener myListener = new PacketListener() {
      public void processPacket(Packet packet) {
        if (!send)
          return;

        Message message = (Message)packet;
        if (message.getType() == Message.Type.groupchat) {
          logD("received message - but it's a groupchat message");
          return;
        }

        if (shouldIgnoreMessage(message))
          return;

        XmppEvent xmppEvent = new XmppEvent();
        xmppEvent.body = message.getBody();
        xmppEvent.sender = message.getFrom();
        xmppEvent.receiver = message.getTo();
        xmppEvent.time = getTime(message);
        if (xmppEvent.time == -1)
          xmppEvent.time = System.currentTimeMillis();
        Object obj = message.getProperty("url");
        if (obj != null && obj instanceof String)
          xmppEvent.url = (String)obj;
        xmppEvent.type = message.getType().toString();
        queueEvent(xmppEvent);
      }

    };
    connection.addPacketListener(myListener, filter);
    PacketFilter presenceFilter = new PacketTypeFilter(Presence.class);
    connection.addPacketListener(new PacketListener() {
      public void processPacket(Packet packet) {
        if (!send)
          return;

        if (!(packet instanceof Presence))
          return;

        Presence presence = (Presence) packet;
        String to = presence.getTo();
        String from = presence.getFrom();
        String presenceType = presence.getType().toString();
        String messageType = "presence";
        if (from == null || presenceType == null)
          return;

        if (to == null || !to.toLowerCase().startsWith(username.toLowerCase() + "@" + domain.toLowerCase()))
          return;
        
        if (to.contains("/") && !to.substring(to.lastIndexOf("/") + 1).toLowerCase().equals(resource.toLowerCase()))
          return;

        XmppEvent xmppEvent = new XmppEvent();
        xmppEvent.type = messageType;
        xmppEvent.body = presenceType;
        xmppEvent.sender = from;
        queueEvent(xmppEvent);
      }
    }, presenceFilter);
  }

  public boolean shouldIgnoreMessage(Message message) {
    String to = message.getTo();
    String from = message.getFrom();
    logD("received message for: " + to + ", body = " + message.getBody() + ", from: " + from);

    if (from == null || from.toLowerCase().equals(username.toLowerCase() + "@" + domain.toLowerCase() + "/" + resource.toLowerCase()))
      return true;  // it's an echo

    if (to == null || !to.toLowerCase().startsWith(username.toLowerCase() + "@" + domain.toLowerCase()))
      return true;
    
    if (to.contains("/") && !to.substring(to.lastIndexOf("/") + 1).toLowerCase().equals(resource.toLowerCase()))
      return true;

    if (message.getBody() == null || message.getBody().trim().length() == 0)
      return true;
    
    return false;
  }
  
  /**
   * IXmpp method implementation - send message over established xmpp connection
   */
  public void sendMessage(String text, String to) {
    logD("sendMessage(" + text + ", " + to + ")");
    if (currentChat == null && (to == null || to.trim().length() == 0)) {
      logD("attempted to send message without being in a chatroom or without specifying recipient");
      return;
    }

    try {
      if (text == null || text.trim().length() == 0)
        return;

      if (connection == null || !connection.isConnected()) {
        logD("sendMessage - not connected, running setUp()");
        connect();
        if (!connection.isAuthenticated()) {
          if (username == null || password == null) {
            logD("not logged in, can't send message");
            return;
          }
          else
            login(username, password);
        }
      }

      Message msg;
      if (to == null || to.trim().length() == 0) {
        msg = currentChat.createMessage();
        msg.setType(Message.Type.groupchat);
      }
      else {
        msg = new Message();
        msg.setType(Message.Type.chat);
      }
      msg.setBody(text);
//        msg.setFrom(username);
      msg.setProperty("time", new Long(System.currentTimeMillis()));
      if (to != null && to.trim().length() != 0) {
        if (!to.contains("@"))
          to += "@" + domain;
//        if (!to.contains("/"))
//          to += "/" + resource;
        msg.setTo(to);
        connection.sendPacket((Packet)msg);
        logD("sending from connection");
      }
      else {
        currentChat.sendMessage(msg);
        logD("sending from chatroom");
      }
    } catch (Throwable e) {
      logD("failed in sendMessage: " + e.getMessage());
      if (!connection.isConnected())
        connect();
      if (!connection.isAuthenticated()) {
        if (username != null && password != null)
          login(username, password);
      }
    }
  }

  /**
   * IXmpp method implementation
   */
  public void setChatRoom(String nickname) {
    logD("setChatRoom(" + nickname + ")");
    if (connection == null)
      return;

    MultiUserChat chat = chats.get(nickname);
    if (chat == null) {
      chat = makeNewChat(nickname);
      chats.put(nickname, chat);
    }

    currentChat = chat;

    // join the room
    try {
      DiscussionHistory dh = new DiscussionHistory();
      dh.setMaxStanzas(0);
      int numTries = 5;
      for (int i = 0; i < numTries && !chat.isJoined(); i++) {
        logD("attempting to join as " + username + " with password " + password + ", try " + i);
        chat.join(username, password, dh, 5000); // 5 seconds timeout
      }
      logD("joined");
    } catch (XMPPException x) {
      logD("failed to join: " + x.getMessage());
    }
  }

  private MultiUserChat makeNewChat(String room) {
    logD("making new chatroom");
    MultiUserChat chat = new MultiUserChat(connection, room);
    chat.addMessageListener(new PacketListener() {
      public void processPacket(Packet packet) {
        if (!send)
          return;

        Message message = (Message) packet;
        if (message.getBody() == null || message.getBody().trim().length() == 0) {
          logD("null body: type = " + message.getType().toString() + ", to = " + message.getTo() + ", from = " + message.getFrom());
          return;
        }

        String from = message.getFrom();
        if (from == null)
          return;

        int slashIndex = from.indexOf("/");
        logD("received message in muc from " + message.getFrom() + ": " + message.getBody());
        if (slashIndex != -1 && from.toLowerCase().substring(slashIndex + 1).equals(username.toLowerCase())) {
          logD("not forwarding - echo message");
          return; // echo
        }

        XmppEvent xmppEvent = new XmppEvent();
        xmppEvent.body = message.getBody();
        xmppEvent.sender = message.getFrom();
        xmppEvent.chatroom = currentChat.getRoom();
        xmppEvent.receiver = null;
        xmppEvent.time = getTime(message);
        if (xmppEvent.time == -1)
          xmppEvent.time = System.currentTimeMillis();
        xmppEvent.type = message.getType().toString();
        queueEvent(xmppEvent);
      }
    });
    return chat;
  }
}